#include<iostream>
#include<string>
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>

using namespace std;

const int basesize = 1024;  // 基准值
const int argvnum = 64; // 定义一个命令最长的长度
const int envnum = 64;
// 全局的命令行参数表
char* gargv[argvnum];
int gargc;

// 我们实现的这个shell，默认的环境变量是继承这个机器的shell的，所以我们自己实现的shell也要有自己的环境变量
char* genv[envnum];

// 全局的退出码变量，使支持查看退出码以及 echo $?
int lastcode = 0;

// 全局的当前的shell工作路径
char pwd[basesize];
char pwdenv[basesize];

// 全局变量与重定向有关
#define NoneRedir   0  // 不重定向
#define InputRedir  1  // 输入重定向
#define OutputRedir 2  // 输出重定向
#define AppRedir    3  // 追加重定向

int redir = NoneRedir;  // 默认不重定向 
char* filename = nullptr;  // 如果要重定向，要指向的文件名

// 命令行提示符需要的三要素
string GetUserName()  // 用户名
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()  // 主机名
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()  // 路径
{
    // 我们在获取当前路径时，不建议从环境变量中直接获取，因为环境变量没有实时更新，导致我们每次命令行打印出来的路径不会变，应该直接从系统中获取，获取成功之后，也要想办法把环境变量更新，
    // string pwd = getenv("PWD");
    // return pwd.empty() ? "None" : pwd;
    // 利用系统调用接口 getcwd() 实时更新系统所在路径
    if(getcwd(pwd,sizeof(pwd)) == nullptr) return "None";
    // 修改环境变量
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);  // PWD=XXX
    return pwd;
}

string MakeCommandLine()
{
    // [ls@VM-24-7-centos myshell]$
    char command_line[basesize];
    snprintf(command_line, basesize, "[%s@%s %s]# ", GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
    return command_line;
}

void PrintCommandLine()  // 1.命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    // 立马刷新
    fflush(stdout);
}

bool GetCommandLine(char command_buffer[], int size)  // size 个字符
{
    // ”ls -a -l“
    // 使用 fgets 函数最方便，所以多定义了一个size
    char* result = fgets(command_buffer, size, stdin);
    if(!result)
        return false;
    command_buffer[strlen(command_buffer)-1] = 0;  // 去掉回车
    if(strlen(command_buffer) == 0) return false;
    return true;
}

void ParseCommandLine(char command_buffer[], int len)
{
    (void)len; // 当传进来的len为0时，会直接返回
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    // 现在开始分割命令，利用strtok,然后分割倒argv里
    const char* sep = " ";
    // strtok的特点：第一次切割的时候要把字符串传进去，第二次及以后要切割第一个参数传的就是null，如果切成功了，会返回第一个被切割的字符串的address，如果失败了就会返回null
    gargv[gargc++] = strtok(command_buffer, sep);  // 第一次切割
    // 一行代码搞定 
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;    
}

// 在shell中
// 有些命令，必须由子进程执行
// 有些命令，不难由子进程执行，要由shell执行 ---内建命令 built command
// 比如我们的cd命令，如果由子进程执行了，就会退出了，实际上不会更改路径
// 所以我们在执行命令的时候，需要先检查是否是内建命令，如果true，就要自己写方法执行，如果false，就直接执行
bool ExecuteCommand()  // 4. 执行命令
{
    // 我们的shell，是不会自己执行命令的，所以要创建子进程执行
    pid_t id = fork();
    if(id < 0) return false;
    if(id == 0)
    {
        // 子进程 
        // 1.执行命令
        // exec*接口:因为我们有了argv表，而且这个表的第一个字符串就是我们所需要命令的名字，所以我们选择execvp
        // 然而，为了所有子进程都能继承我们的环境变量表，所以我们选择vpe，也传入我们shell的环境变量
        // 以后运行我们自己的程序执行时，也会获得我们myshell进程的环境变量了
        execvpe(gargv[0], gargv, genv);
        // 2.退出
        exit(1);
    }
    // 父进程，等待子进程退出
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        if(WIFEXITED(status))  // 如果子进程正常退出
        {
            lastcode = WEXITSTATUS(status);  // 返回退出码
        }
        else  // 子进程代码没跑完，设置错误码为100
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}

void AddEnv(const char* item)
{
    int index = 0;
    while(genv[index])
        index++;
    genv[index] = (char*)malloc(strlen(item)+1);
    strcpy(genv[index], item);
    genv[++index] = nullptr;
}

// shell自己执行命令，本质上是调用自己的函数
bool CheckAndExecBuiltCommand()
{
    if(strcmp("cd", gargv[0]) == 0)  // 如果是cd命令，自己执行
    {
        if(gargc == 2)  // 一个cd，另一个是路径
        {
            chdir(gargv[1]);  // 手动改变cwd
            lastcode = 0;
        }
        else
        {
            lastcode = 1;  // 让 1 表示cd出问题的退出码
        }
        return true;
    }
    else if(strcmp("export", gargv[0]) == 0)  // export 命令也是内建命令，由shell自己执行
    {
        if(gargc == 2)
        {
            AddEnv(gargv[1]);  // 加入到环境变量表中
            lastcode = 0;
        }
        else 
        {
            lastcode = 2; // 让 2 表示导入环境变量出错时的退出码
        }
        return true;
    }
    else if(strcmp("env", gargv[0]) == 0)  // env 也是要查shell自己的环境变量列表，所以也算是一个内建命令
    {
        for(int i = 0;genv[i];i++)
        {
            printf("%s\n", genv[i]);
        }
        lastcode = 0;
        return true;
    }
    else if(strcmp("echo", gargv[0]) == 0)  // echo 命令也是内建命令，因为它能查到本地变量
    {
        if(gargc == 2)
        {
            // echo hello  打印一个 hello
            // echo $?  查看最近一个进程的退出码
            // echo $a  查看本地变量
            if(gargv[1][0] == '$')
            {
                if(gargv[1][1] == '?')
                {
                    // 查看退出码
                    printf("%d\n", lastcode);
                    lastcode = 0;   // 记得退出码设为0
                }
            }
            else 
            {
                printf("%s\n", gargv[1]);
                lastcode = 0;
            }
        }
        else 
        {
            lastcode = 3;   // 错误码设为3
        }
        return true;    
    }
    return false;
}

// 创建myshell的环境变量表,作为一个shell，其实应该从系统的配置文件中获取环境变量
// 但是我们做不到，只能从父shell中拷贝一份下来
void InitEnv()
{
    extern char** environ;
    int index = 0;
    while(environ[index])
    {
        genv[index] = (char*)malloc(strlen(environ[index])+1);
        strcpy(genv[index], environ[index]);
        index++;
    }
    genv[index] = nullptr;
}


int main()
{
    InitEnv();  // 环境变量
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine();  // 1. 命令行提示符
        
        if(!GetCommandLine(command_buffer, basesize))  // 2. 获取用户命令：不难发现，我们从命令行打出的命令其实是一个个的字符串，所以存入一个数组中
        {
            continue;
        }
        //print("%s\n", command_buffer);  // 这里可以不用带 \n，因为我们输入命令的时候，最后一定会打一个回车(换行)，所以算是一个command_buffer中的一个字符，如果要带\n,就得去函数里做一个修改，把我们打的回车去掉
        ParseCommandLine(command_buffer, strlen(command_buffer));  // 3. 分析命令:"ls -a -l --color" -> "ls" "-a" "-l" "--color",使用execl*函数
        
        if(CheckAndExecBuiltCommand())
        {
            continue;  // 是内建命令，在函数里执行后继续执行
        }
        
        ExecuteCommand();  // 4. 执行命令
    }
    return 0;
}
